package xyz.lwm.lazycat.utility.scan;

import xyz.lwm.lazycat.utility.Assert;
import xyz.lwm.lazycat.utility.StringUtil;
import xyz.lwm.lazycat.utility.cache.WeakCaches;
import xyz.lwm.lazycat.utility.exception.UtilException;
import xyz.lwm.lazycat.utility.reflect.ClassUtil;
import xyz.lwm.lazycat.utility.resource.FileResource;
import xyz.lwm.lazycat.utility.resource.Location;
import xyz.lwm.lazycat.utility.resource.Resource;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.function.Predicate;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;

/**
 * <h2>pattern 扫描器</h2>
 * <p>利用通配符等获取资源</p>
 * <pre class="code">
 * PatternScanner patternScanner = new PatternScanner();
 * Set<Resource> set = patternScanner.scan(
 *        "classpath:com/massage/avaje/**&#47;scan/*.class",
 *        resource -> resource.getName().endsWith(".class")
 * );
 * </pre>
 * <pre>
 * 说明:
 * 1. 扫描的资源，可以是文件，也可以是jar包中的文件；
 * 2. 扫描的通配符，支持通配符：*、?、**
 * 3. * 通配符，匹配任意字符，'/' 除外
 * </pre>
 *
 * @author lwm
 */
public class PatternResourceScanner implements Scanner<String, Resource> {

    private static final String KEY_SUFFIX = UUID.randomUUID().toString();

    private final ClassLoader classLoader = ClassUtil.getDefaultClassLoader();

    @Override
    public List<Resource> scan(String source, Predicate<Resource> filter) {
        Assert.notEmpty(source, "scanned source");

        List<Resource> list = new ArrayList<>();

        // get location pattern
        LocationPattern locationPattern = LocationPattern.parse(source);
        // get path matcher
        ResourceMatcher matcher = createMatcher(locationPattern);

        if (locationPattern.getLocation() == Location.FILEPATH) {
            scanFile(list, locationPattern, filter.and(matcher));
        }
        if (locationPattern.getLocation() == Location.CLASSPATH) {
            scanClasspath(list, locationPattern, filter.and(matcher));
        }

        // sort, file resource is before class resource
        list.sort(Comparator.comparingInt(this::getOrder));

        return list;
    }

    private int getOrder(Resource resource) {
        return resource instanceof FileResource ? 0 : 1;
    }

    // create path matcher, it could customize
    protected ResourceMatcher createMatcher(LocationPattern locationPattern) {
        return new ResourceMatcher(locationPattern);
    }

    private void scanFile(List<Resource> list, LocationPattern locationPattern, Predicate<Resource> filter) {
        String location = locationPattern.getPath();
        if (StringUtil.isNotEmpty(location)) {
            File file = new File(location);
            scanFileProtocol(file, list, filter, Location.FILEPATH);
        }
    }

    private void scanClasspath(List<Resource> list, LocationPattern locationPattern, Predicate<Resource> filter) {
        // get resources
        List<URL> resources = getResources(locationPattern);
        for (URL url : resources) {
            if (url != null) {
                if (FILE_PROTOCOL.equals(url.getProtocol())) {
                    scanFileProtocol(new File(decode(url.getFile())), list, filter, Location.CLASSPATH);
                } else if (JAR_PROTOCOL.equals(url.getProtocol())) {
                    scanJarProtocol(getJarFile(url), list, filter);
                }
            }
        }
    }

    private void scanFileProtocol(File file, List<Resource> list, Predicate<Resource> filter, Location location) {
        // get resource
        String classpath = location == Location.CLASSPATH ? getClassPath(file.getPath()) : null;
        Resource resource = Resource.createResource(file, classpath);
        if (resource.exists() && filter.test(resource)) {
            list.add(resource);
        }
        if (file.isDirectory()) {
            File[] files = file.listFiles();
            if (files != null) {
                for (File f : files) {
                    scanFileProtocol(f, list, filter, location);
                }
            }
        }
    }

    private void scanJarProtocol(JarFile jarFile, List<Resource> list, Predicate<Resource> filter) {
        Enumeration<JarEntry> entries = jarFile.entries();
        while (entries.hasMoreElements()) {
            JarEntry jarEntry = entries.nextElement();
            // the size of dir in jar is zero,
            // and the size of file in jar is greater than zero
            // get resource
            Resource resource = Resource.createResource(jarEntry.getName(), jarFile);
            if (filter.test(resource)) {
                list.add(resource);
            }
        }
    }

    /**
     * @param locationPattern "a/b"
     * @return Enumeration
     */
    private List<URL> getResources(LocationPattern locationPattern) {
        try {
            ArrayList<URL> list = Collections.list(classLoader.getResources(locationPattern.getPath()));
            if (StringUtil.isBlank(locationPattern.getPath())) {
                list.addAll(Collections.list(classLoader.getResources("META-INF/MANIFEST.MF")));
            }
            return list;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private JarFile getJarFile(URL url) {
        try {
            JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
            return jarURLConnection.getJarFile();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private String decode(String content) {
        try {
            return URLDecoder.decode(content, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new UtilException(e);
        }
    }

    // get user dir
    private String getUserDir() {
        return System.getProperty(USER_DIR_ENV_NAME);
    }

    // get classpath set
    @SuppressWarnings("unchecked")
    private Set<String> getClassRootPathSet() {
        return (Set<String>) WeakCaches.getCache(PatternResourceScanner.class.getName() + KEY_SUFFIX)
                .get(CLASS_PATH_ENV_NAME, k -> {
                    String classPaths = System.getProperty((String) k);
                    String userDir = getUserDir();
                    return StringUtil.split(classPaths, File.pathSeparator, "", "",
                            s -> Paths.get(s).isAbsolute() ? Paths.get(s).normalize().toString() :
                                    Paths.get(userDir, s).normalize().toString()
                    ).stream().filter(StringUtil::isNotBlank).collect(Collectors.toSet());
                });
    }

    // get classpath
    private String getClassPath(String path) {
        Path filePath = Paths.get(path);
        Set<String> classRootPaths = getClassRootPathSet();
        for (String rootPath : classRootPaths) {
            if (filePath.startsWith(Paths.get(rootPath))) {
                return path.substring(rootPath.length());
            }
        }
        return null;
    }

}
